Real World Haskell 第六章 使用类型类

您所在的位置:网站首页 haskell 类型类 Real World Haskell 第六章 使用类型类

Real World Haskell 第六章 使用类型类

2024-07-16 14:11| 来源: 网络整理| 查看: 265

类型类是Haskell中最强大的特性之一。它可以让你定义一个泛型接口,来给很多不同类型提供一个通用的特性集。类型类是一些语言基本特性如相等性测试和数字操作的核心。在讨论类型类到底是什么之前,我们先要解释下为什么需要它们。为何需要类型类让我们想象一下,由于某种深奥的原因Haskell语言的设计者忽略了相等性测试 ==的实现。听到这个消息后,在震惊之余你毅然决然的决定去实现你自己的相等性测试。你的应用由一个简单的Color类型组成,因此你首先为这个类型实现相等性测试。初次尝试如下:-- file: ch06/naiveeq.hsdata Color = Red | Green | BluecolorEq :: Color -> Color -> BoolcolorEq Red   Red   = TruecolorEq Green Green = TruecolorEq Blue  Blue  = TruecolorEq _     _     = False用ghci测试:ghci> :load naiveeq.hs[1 of 1] Compiling Main             ( naiveeq.hs, interpreted )Ok, modules loaded: Main.ghci> colorEq Red RedTrueghci> colorEq Red GreenFalse现在,假设你要对字符串添加相等性测试。由于Haskell的字符串是一个字符的列表,我们可以写一个简单的函数来进行这个测试。为了简单起见,我们这里偷用下==操作符来进行演示。-- file: ch06/naiveeq.hsstringEq :: [Char] -> [Char] -> Bool-- 如果两个都是空则匹配stringEq [] [] = True-- 如果都以相同的字符开头,则检查剩下的字符串stringEq (x:xs) (y:ys) = x == y && stringEq xs ys-- 其他情况均不匹配stringEq _ _ = False现在应该可以看出一个问题:我们必须为要进行比较的不同类型使用不同名字的函数。这很低效且令人厌烦。如果能够只使用 ==来比较任何东西将方便很多。== 在实现像 /=这样的泛型函数也很有用,几乎对任何事物都有效。有了一个可以比较任意事物的函数,我们可以让自己的代码更通用:如果一段代码只是需要比较一些事物,它就应该可以接受任意数据类型,编译器知道如何比较它们。并且,如果后来增加了新的数据类型,已有的代码不需要进行修改。Haskell的类型类被设计来做这些事情。什么是类型类类型类定义一组函数,这些函数依赖于所给定的数据的类型可以有不同的实现。类型类看上去可能像面向对象编程中的对象,但是它们确实很不相同。让我们用类型类来解决本章前面的相等性测试的难题。首先,必须定义类型类本身。我们想要一个函数,它接受两个相同类型的参数并返回一个Bool值来表示两个参数是否相等。我们不关心参数的类型是什么,只是需要这种类型的两个项。这是我们的类型类的第一个定义:-- file: ch06/eqclasses.hsclass BasicEq a where   isEqual :: a -> a -> Bool这里声明了一个名为BasicEq的类型类,用字母a表示实例的类型。这个类型类的实例是任何实现了其中所定义的函数的类型。这个类型类定义了一个函数。这个函数取两个参数-都是这种实例的类型-并且返回一个Bool值。[Note]    此“类”非彼“类”Haskell中定义类型类的关键字是 class。不幸的是这可能会让来自面向对象编程的人们搞混,因为我们所说的并不是一回事!第一行里面参数名a是任意选择的。可以用任何名字。关键点是,在函数中要列出类型时,必须用实例的名字。在ghci中看一下。可以在ghci中输入 :type 来显式某物的类型。看下对 isEqual是怎么说的:*Main> :type isEqualisEqual :: (BasicEq a) => a -> a -> Bool可以这样读:“对于说有BasicEq 的实例类型a, isEqual 取两个a类型参数,并返回一个Bool类型值。”看下如何对特定类型定义 isEqual函数。-- file: ch06/eqclasses.hsinstance BasicEq Bool where   isEqual True  True  = True   isEqual False False = True   isEqual _     _     = False可以用ghci来验证现在可以在 Bool 上使用 isEqual了,但是其他类型不行。ghci> :load eqclasses.hs[1 of 1] Compiling Main             ( eqclasses.hs, interpreted )Ok, modules loaded: Main.ghci> isEqual False FalseTrueghci> isEqual False TrueFalseghci> isEqual "Hi" "Hi":1:0:   No instance for (BasicEq [Char])     arising from a use of `isEqual' at :1:0-16   Possible fix: add an instance declaration for (BasicEq [Char])   In the expression: isEqual "Hi" "Hi"   In the definition of `it': it = isEqual "Hi" "Hi"注意,当我们尝试比较两个字符串时,ghci注意到我们并没有为String提供BasicEq的实例。因此它并不知道如何比较String,所以它建议我们为 [Char]定义BasicEq 的实例来解决这个问题,[Char]与String是一回事。在“声明类型类实例”一节将深入到定义实例的细节。不过首先还是继续看下如何定义类型类。在这个例子里,不等于函数是有用的。这里是在一个类型类中定义两个函数。-- file: ch06/eqclasses.hsclass BasicEq2 a where   isEqual2    :: a -> a -> Bool   isNotEqual2 :: a -> a -> Bool要提供一个BasicEq2的实例的话,需要定义两个函数: isEqual2 和 isNotEqual2。虽然BasicEq2的定义是可以的,但是看上去好像在自找麻烦。逻辑上讲,如果我们知道了 isEqual或者 isNotEqual 的返回值,我们就知道另一个函数的结果,对所有的类型都是如此。与其强迫类型类的使用者去为所有类型类都写两个函数,我们不如提供他们的默认实现。这样用户们就只需要定义其中一个函数。这个例子显示了如何做。-- file: ch06/eqclasses.hsclass BasicEq3 a where   isEqual3 :: a -> a -> Bool   isEqual3 x y = not (isNotEqual3 x y)   isNotEqual3 :: a -> a -> Bool   isNotEqual3 x y = not (isEqual3 x y)要实现这个类型类必须提供至少一个函数的实现。也可以两个都提供,但并不需要。我们给两个函数都提供了默认实现,每个函数分别依赖于另一个的计算结果。如果我们不至少指定一个的话,结果将是个死循环。因此,必须实现至少一个函数。BasicEq3里提供了一个和Haskell内置的 == 和 /= 操作符非常像的类。实际上,这些操作符在一个类型类中的定义与BasicEq3 几乎完全相同。Haskell 98Report中定义了一个实现相等性比较的类型类。这是内置的Eq类型类的定义。注意它与我们的 BasicEq3 类型类多么相似。class  Eq a  where   (==), (/=) :: a -> a -> Bool      -- 最小的完整实现:      --     (==) or (/=)   x /= y     =  not (x == y)   x == y     =  not (x /= y)声明类型类实例现在知道了如何定义类型类,该是时候来学习如何定义类型类的实例了。类型通过实现一个类型类必要的函数来成为此类型类的实例。在“对类型类的需求”一节我们尝试创建Color类型的相等性测试。现在来看下如何让相同的 Color 类型成为 BasicEq3 类的成员。-- file: ch06/eqclasses.hsinstance BasicEq3 Color where   isEqual3 Red Red = True   isEqual3 Green Green = True   isEqual3 Blue Blue = True   isEqual3 _ _ = False注意我们提供了与“对类型类的需求”一节中差不多相同的函数。实际上,实现是相同的。用这种方法可以让我们定义的任何类型成为BasicEq3 的实例,不止是Color类型。用这种基本模式,可以把从数字到图形的任何东西都定义相等性测试。实际上,在“相等性,顺序性和可比较性”一节可以看到,这就是Haskell的== 操作符可以操作你的自定义类型的原因。还要注意 BasicEq3 定义了 isEqual3 和 isNotEqual3,但在 Color 实例中我们只实现了其中一个。这是因为BasicEq3 中包含默认实现。由于我们没有明确给出 isNotEqual3 的定义,因此编译器自动使用 BasicEq3声明中给出的默认实现。重要的内建类型类现在我们已经讨论了如何定义自己的类型类,以及如何把类型声明为类型类的实例。现在该介绍标准 HaskellPrelude中内置的类型类了。本章开头提到类型类是这门语言最核心的几个概念之一。我们这里讨论最常用的几个。更多细节可以查看Haskell库参考文档。将给出类型类的描述,并且会告诉你要实现其定义必须要实现哪些函数。Show类型类Show把值转换成String。可能最常用的就是把一个数字转换成String,不过它在很多类里都有定义,因此可以用它转换很多种类型的数据。如果你自己定义了类型,把他们变成Show的实例,可以方便的在ghci中显示或者在程序中输出他们。Show 中最重要的函数是 show。它取一个参数:要转换的数据。返回一个表示数据的字符串。ghci显示show的类型如下:ghci> :type showshow :: (Show a) => a -> String看一些把值转换成字符串的例子:ghci> show 1"1"ghci> show [1, 2, 3]"[1,2,3]"ghci> show (1, 2)"(1,2)"记住ghci显示的结果和输入一个Haskell程序的格式是一致的。因此表达式show 1 返回包含一个字符1的字符串。也就是说,引号并不是字符串的一部分。可以用 putStrLn 来清楚的看到这一点。ghci> putStrLn (show 1)1ghci> putStrLn (show [1,2,3])[1,2,3]也可以把show 用在String上。ghci> show "Hello!""\"Hello!\""ghci> putStrLn (show "Hello!")"Hello!"ghci> show ['H', 'i']"\"Hi\""ghci> putStrLn (show "Hi")"Hi"ghci> show "Hi, \"Jane\"""\"Hi, \\\"Jane\\\"\""ghci> putStrLn (show "Hi, \"Jane\"")"Hi, \"Jane\""在String上执行show看上去很混乱。因为show产生的结果是一个Haskell的字面量,show加上了引号和转义符号,这样就可以在Haskell程序中包含他们了。ghci也用show来显示结果,因此引号和转义符被加了两次。用putStrLn 可以帮助看清这些区别。可以方便的给自己的类型定义Show的实例。这是个例子。-- file: ch06/eqclasses.hsinstance Show Color where   show Red   = "Red"   show Green = "Green"   show Blue  = "Blue"这个例子给我们的Color类型定义了Show的实例(看“对类型类的需求”一节)。实现是简单的:定义了一个show函数,这就是所需做的了。[Note] Show 类型类Show经常用来定义数据的字符串表示,方便机器用Read读入解析。要显示给最终用户看的话,表示方式可能与Show期望的不同,这时Haskell程序员一般会写自定义函数来格式化数据。ReadRead 类型类基本上是 Show的反面:它定义了读取String解析并返回Read成员类型的函数。Read中最重要的函数是read。可以让ghci显示它的类型:ghci> :type readread :: (Read a) => String -> a这个例子演示了read和show的使用:-- file: ch06/read.hsmain = do       putStrLn "Please enter a Double:"       inpStr a,而show期望的值类型为 Show a =>a。有很多类型同时具有Read和Show的实例。不知道特定的类型,编译器就必须从这些类型里猜测需要用哪个。在这种情况下,经常选择Integer。如果希望允许浮点型数据输入,这就无法工作,因此我们提供明确的类型。[Tip]    关于默认的说明大多数情况下,如果没有明确的Double类型注解,编译器无法猜测一个通用的类型,并返回错误。这里默认为Integer是因为字面量 2默认被当作Integer,除非指定其他的类型。尝试从ghci命令行使用read也是同样的效果。ghci要用show来显示结果,因此你可能遇到这种类型混淆的问题。你需要明确的给出要读取的结果的类型,如下:ghci> read "5":1:0:   Ambiguous type variable `a' in the constraint:     `Read a' arising from a use of `read' at :1:0-7   Probable fix: add a type signature that fixes these type variable(s)ghci> :type (read "5")(read "5") :: (Read a) => aghci> (read "5")::Integer5ghci> (read "5")::Double5.0回想read的类型: (Read a) => String -> a 。这里的a是Read的每种实例。要调用哪一个特定的解析函数取决于read期望的结果值的类型。我们看下这是如何工作的:ghci> (read "5.0")::Double5.0ghci> (read "5.0")::Integer*** Exception: Prelude.read: no parse注意当要把 5.0 当作Integer解析时的错误。当期待的返回值是Integer时,解析器选取了不同于期待Double时的Read实例。Integer的解析器不能接受小数点,因此抛出了一个异常。Read类提供了一些非常复杂的解析器。你可以通过提供readsPrec函数的实现来定义简单的解析器。当你的实现成功解析时,返回一个包含一个元组的列表,当解析不成功时返回空列表。这是一个实现的例子:-- file: ch06/eqclasses.hsinstance Read Color where   -- readsPrec 是解析的主函数   readsPrec _ value =       -- 给tryParse 传递一个数对的列表。每个数对有一个字符串和要返回的值       -- tryParse会尝试将输入与这些字符串进行匹配。       tryParse [("Red", Red), ("Green", Green), ("Blue", Blue)]       where tryParse [] = []    -- 如果没有可以尝试的了,失败             tryParse ((attempt, result):xs) =                     -- 将要匹配的字符串的开始与要寻找的文本进行匹配                     if (take (length attempt) value) == attempt                        -- 如果匹配了,返回结果和剩下的输入                        then [(result, drop (length attempt) value)]                        -- 如果没有匹配,尝试列表中的下一个数对                        else tryParse xs这个例子处理三个已知的颜色。对齐它的返回空列表(结果产生“noparse”信息)。这个函数应该将没有解析的部分返回,这样系统就可以把不同的类型解析结果合在一起。这是使用这个新的Read实例的例子:ghci> (read "Red")::ColorRedghci> (read "Green")::ColorGreenghci> (read "Blue")::ColorBlueghci> (read "[Red]")::[Color][Red]ghci> (read "[Red,Red,Blue]")::[Color][Red,Red,Blue]ghci> (read "[Red, Red, Blue]")::[Color]*** Exception: Prelude.read: no parse注意最后一次尝试时的错误。那时因为我们的解析器还不够聪明,还不能处理开头的空格。如果我们修改解析器让它接受开头的空格,那个尝试将会成功。可以通过修改你的Read实例,让它把开头的空格去掉,就可以纠正这个问题,这在Haskell中是很常见的。[Tip]    Read 用得并不广虽然有可能用Read类型类构造成熟的解析器,但是用Parsec要更容易,Read只用来做简单的任务。在第16章 使用 Parsec中将详细介绍Parsec。用Read 和 Show串行化你可能经常有些内存中的数据结构需要保存在磁盘上,供日后读取或者通过网络传输。把内存中的数据转换成一系列字节存储的过程叫做串行化。read和show是串行化很好的工具。show产生的结果人和机器都可读。大部分show的输出也具有有效的Haskell语法,这是写Show实例的人有意为之的。[Tip] 解析大的字符串在Haskell中处理字符串一般是惰性的,因此read和show可以用在非常大的数据结构上而不会出错误。Haskell内置的read和show是很高效的,用纯Haskell实现。第九章《错误处理》有如何处理解析异常的信息。在ghci中试一下:ghci> let d1 = [Just 5, Nothing, Nothing, Just 8, Just 9]::[Maybe Int]ghci> putStrLn (show d1)[Just 5,Nothing,Nothing,Just 8,Just 9]ghci> writeFile "test" (show d1)首先,我们把一个列表赋值到d1。然后输出 show d1的结果,我们可以看到它生成了什么。然后,我们把show d1 的结果写入到名为 test 的文件中。我们试着读回来。ghci> input let d2 = read input:1:9:   Ambiguous type variable `a' in the constraint:     `Read a' arising from a use of `read' at :1:9-18   Probable fix: add a type signature that fixes these type variable(s)ghci> let d2 = (read input)::[Maybe Int]ghci> print d1[Just 5,Nothing,Nothing,Just 8,Just 9]ghci> print d2[Just 5,Nothing,Nothing,Just 8,Just 9]ghci> d1 == d2True首先,让Haskell把文件读回。然后尝试将read input的结果赋值给d2。这会生成一个错误。因为解释器不知道d2应该是什么类型,因此它不知道如何解析输入。如果给它一个明确的类型,它可以工作,并且我们可以验证两个数据是相同的。因为非常多不同的类型默认是Read 和 Show的实例(并且其他的也可以很容易的变成他们的实例,看:“自动继承”一节),可以把它用在很多复杂的数据类型上。这里是一些稍微复杂些的数据结构的例子:ghci> putStrLn $ show [("hi", 1), ("there", 3)][("hi",1),("there",3)]ghci> putStrLn $ show [[1, 2, 3], [], [4, 0, 1], [], [503]][[1,2,3],[],[4,0,1],[],[503]]ghci> putStrLn $ show [Left 5, Right "three", Left 0, Right "nine"][Left 5,Right "three",Left 0,Right "nine"]ghci> putStrLn $ show [Left 0, Right [1, 2, 3], Left 5, Right []][Left 0,Right [1,2,3],Left 5,Right []]Numeric 类型Haskell有一组非常强大的数字类型。从快速的32位或64位整数到任意精度的有理数都可以使用。你可能已经知道了像+ 这样的操作符可以对所有这些类型使用。这个特性是用类型类实现的。作为附加的好处,它允许你定义自己的数字类型,并把它作为Haskell中的一等公民。在开始讨论数字类型时,先查看下这些类型本身。表6-1“Numeric类型精选”描述了Haskell中最常用的数字类型。注意还有些其他的数字类型可用,用来实现与C交互等特定用途。Table 6.1. Numeric 类型精选类型         描述Double    双精度浮点数。浮点数据的一般选择Float    单精度浮点数。与C交互时经常使用Int    带符号固定精度整数;最小范围 [-2^29..2^29-1]Int8    带符号8位整数Int16 带符号16位整数Int32 带符号32位整数Int64 带符号64位整数Integer 任意精度带符号整数;取值范围只受机器资源限制。常用Rational    任意精度有理数。存储为两个Integer的比值Word    固定精度无符号整数;与Int存储空间相同Word8    8位无符号整数Word16    16位无符号整数Word32    32位无符号整数Word64    64位无符号整数这些是相当多不同的数字类型。有一些像相加这样的操作,可以用在所有这些类型上。还有其他的入asin,只用在浮点数类型上。表6-2“Numeric函数和常量精选”里总结了操作不同数字类型的函数,表6-3“Numeric类的类型类实例”将类型与它们相应的类型类进行匹配。在读这个表时,记住Haskell的操作符只是函数: (+) 2 3 或者2 + 3 结果都一样。按照习惯,把操作符作为函数时,写在括号里。Table 6.2. “Numeric函数和常量精选”Item    Type    Module    Description项目   类型  模块   描述(+)    Num a => a -> a -> a    Prelude    相加(-)    Num a => a -> a -> a    Prelude    相减(*)    Num a => a -> a -> a    Prelude    相乘(/)    Fractional a => a -> a -> a    Prelude    小数除法(**)    Floating a => a -> a -> a    Prelude    乘方(^)    (Num a, Integral b) => a -> b -> a    Prelude    非负整数乘方(^^)    (Fractional a, Integral b) => a -> b -> a    Prelude    对小数求任意整数乘方(%)    Integral a => a -> a -> Ratio a    Data.Ratio    生成比值(.&.)    Bits a => a -> a -> a    Data.Bits    位与(.|.)    Bits a => a -> a -> a    Data.Bits    位或abs    Num a => a -> a    Prelude    绝对值approxRational    RealFrac a => a -> a -> Rational    Data.RatioApproximate rational composition based on fractional numerators anddenominatorscos    Floating a => a -> a    Prelude    Cosine. Also provided areacos, cosh, and acosh, with the same type.div    Integral a => a -> a -> a    Prelude    Integer division alwaystruncated down; see also quotfromInteger    Num a => Integer -> a    Prelude    Conversion from anInteger to any numeric typefromIntegral    (Integral a, Num b) => a -> b    Prelude    Moregeneral conversion from any Integral to any numeric typefromRational    Fractional a => Rational -> a    Prelude    Conversionfrom a Rational. May be lossy.log    Floating a => a -> a    Prelude    Natural logarithmlogBase    Floating a => a -> a -> a    Prelude    Log with explicit basemaxBound    Bounded a => a    Prelude    The maximum value of a bounded typeminBound    Bounded a => a    Prelude    The minimum value of a bounded typemod    Integral a => a -> a -> a    Prelude    整数取模pi    Floating a => a    Prelude    数学常数 圆周率piquot    Integral a => a -> a -> a    Prelude    Integer division;fractional part of quotient truncated towards zerorecip    Fractional a => a -> a    Prelude    倒数rem    Integral a => a -> a -> a    Prelude    整数相除取余数round    (RealFrac a, Integral b) => a -> b    Prelude    取整到最接近的整数shift    Bits a => a -> Int -> a    Bits    左移指定位,与右移相反sin    Floating a => a -> a    Prelude    正弦。对相同类型也提供了 asin, sinh, and asinh,。sqrt    Floating a => a -> a    Prelude    平方根tan    Floating a => a -> a    Prelude    Tangent. Also provided areatan, tanh, and atanh, with the same type.toInteger    Integral a => a -> Integer    Prelude    Convert anyIntegral to an IntegertoRational    Real a => a -> Rational    Prelude    Convert losslesslyto Rationaltruncate    (RealFrac a, Integral b) => a -> b    Prelude    Truncatesnumber towards zeroxor    Bits a => a -> a -> a    Data.Bits    按位异或Table 6.3. Typeclass Instances for Numeric TypesType    Bits    Bounded    Floating    Fractional    Integral    Num Real    RealFracDouble              X    X         X    X    XFloat              X    X         X    X    XInt    X    X              X    X    XInt16    X    X              X    X    XInt32    X    X              X    X    XInt64    X    X              X    X    XInteger    X                   X    X    XRational or any Ratio                   X         X    X    XWord    X    X              X    X    XWord16    X    X              X    X    XWord32    X    X              X    X    XWord64    X    X              X    X    X经常需要在数字类型之间进行转换。表6-2“”列出了很多可以用来做转换的函数。然而如何在任意两个类型间进行转换并不总是显而易见的。表6-4“在Numeric类型间转换”提供了如何在不同类型间转换的信息。Table 6.4. Conversion Between Numeric TypesSource Type    Destination TypeDouble, Float    Int, Word    Integer    RationalDouble, Float    fromRational . toRational    truncate *    truncate *  toRationalInt, Word    fromIntegral    fromIntegral    fromIntegral    fromIntegralInteger    fromIntegral    fromIntegral    N/A    fromIntegralRational    fromRational    truncate *    truncate *    N/A可以用 round, ceiling 或者 floor 替代 truncate。在“扩展实例:Numeric类型”一节有其他的例子演示如何使用这些数字类型类。相等性,顺序性,及比较我们已经说过了算术操作符如 + 可以用在所有不同的数字上。但是Haskell里还有些更加广泛的操作符。最明显的当然是相等性测试: == 和/=。这些操作符定义在 Eq 类里。还有 >= 和 show Red"Red"ghci> (read "Red")::ColorRedghci> (read "[Red,Red,Blue]")::[Color][Red,Red,Blue]ghci> (read "[Red, Red, Blue]")::[Color][Red,Red,Blue]ghci> Red == RedTrueghci> Red == BlueFalseghci> Data.List.sort [Blue,Green,Blue,Red][Red,Green,Blue,Blue]ghci> Red < BlueTrue注意Color排序的顺序基于构造子定义的顺序。自动继承并不是总是可能的。例如,如果你定义了一个类型  data MyType = MyType (Int -> Bool),编译器将不能继承Show的实例,因为它不知道如何渲染一个函数。这时将会得到一个编译错误。当从某些类型类自动继承实例时,我们的data声明中引用的类型也必须是这种类型类的实例(手工或者自动)。-- file: ch06/AutomaticDerivation.hsdata CannotShow = CannotShow               deriving (Show)-- will not compile, since CannotShow is not an instance of Showdata CannotDeriveShow = CannotDeriveShow CannotShow                       deriving (Show)data OK = OKinstance Show OK where   show _ = "OK"data ThisWorks = ThisWorks OK                deriving (Show)实际使用类型类:让JSON更易用在“Haskell中表达JSON数据”一节中介绍的JValue类型并不太容易使用。这里是一段删节重整过的实际JSON数据段,由一个知名的搜索引擎生成的。{ "query": "awkward squad haskell", "estimatedCount": 3920, "moreResults": true, "results": [{   "title": "Simon Peyton Jones: papers",   "snippet": "Tackling the awkward squad: monadic input/output ...",   "url": "http://research.microsoft.com/~simonpj/papers/marktoberdorf/",  },  {   "title": "Haskell for C Programmers | Lambda the Ultimate",   "snippet": "... the best job of all the tutorials I've read ...",   "url": "http://lambda-the-ultimate.org/node/724",  }]}这里是Haskell中的表示。-- file: ch05/SimpleResult.hsimport SimpleJSONresult :: JValueresult = JObject [ ("query", JString "awkward squad haskell"), ("estimatedCount", JNumber 3920), ("moreResults", JBool True), ("results", JArray [    JObject [     ("title", JString "Simon Peyton Jones: papers"),     ("snippet", JString "Tackling the awkward ..."),     ("url", JString "http://.../marktoberdorf/")    ]]) ]因为Haskell并不支持包含不同类型值的列表,我们不能直接表示包含不同类型值的JSON对象。我们需要把值用JValue构造子包装起来。这限制了灵活性:如果想把3920从数字转成字符串“2,920”的话,必须把构造子从JNumber 转成 JString。Haskell的类型类对这个问题提供了很吸引人的解决方法。-- file: ch06/JSONClass.hstype JSONError = Stringclass JSON a where   toJValue :: a -> JValue   fromJValue :: JValue -> Either JSONError ainstance JSON JValue where   toJValue = id   fromJValue = Right现在我们应用toJValue函数,而不是应用像JNumber这样的构造子来包装值。如果改变了一个值的类型,编译器将选择一个适合的toJValue实现。我们还提供了一个 fromJValue函数,它尝试把JValue值转成我们需要的类型。更有用的错误fromJValue的返回类型用了 Either 类型。像Maybe一样,这个类型是预定义的,经常用来表示可能会失败的计算。虽然Maybe也用作这个目的,但是它在发生错误时没有提供任何信息:我们确实只有Nothing。Either类型有个类似的结构,但是“有些错误发生”的构造子是 Left而不是Nothing,而且它带一个参数。-- file: ch06/DataEither.hsdata Maybe a = Nothing            | Just a              deriving (Eq, Ord, Read, Show)data Either a b = Left a               | Right b                 deriving (Eq, Ord, Read, Show)a参数用的类型经常是 String,在出现问题时用来提供有用的描述。要看Either类型实际上如何用的,我们来看下我们的类型类的简单实例。-- file: ch06/JSONClass.hsinstance JSON Bool where   toJValue = JBool   fromJValue (JBool b) = Right b   fromJValue _ = Left "not a JSON boolean"创建实例的type同义词Haskell98标准不允许用下面的格式写一个实例,虽然它看上去很合理。-- file: ch06/JSONClass.hsinstance JSON String where   toJValue               = JString   fromJValue (JString s) = Right s   fromJValue _           = Left "not a JSON string"String是 [Char]的同义词,这样就把 [a]中类型参数a替换成了Char。根据Haskell98的规则,在写实例时不能给类型参数提供一个类型。换句话说,为[a]写一个实例是合法的,但是[Char]不行。虽然GHC默认遵守Haskell98标准,但是可以通过在源文件开头加上特定格式的注释来解除这个限制。-- file: ch06/JSONClass.hs{-# LANGUAGE TypeSynonymInstances #-}这个注释是编译器指令,称为 pragma,它告诉编译器允许语言扩展。TypeSynonymInstances语言扩展可以让上面的代码合法。这一章将会遇到其他一些语言扩展,在本书后面还有一些。生活在开放的世界Haskell的类型类被设计成允许随时创建类型类的新实例。-- file: ch06/JSONClass.hsdoubleToJValue :: (Double -> a) -> JValue -> Either JSONError adoubleToJValue f (JNumber v) = Right (f v)doubleToJValue _ _ = Left "not a JSON number"instance JSON Int where   toJValue = JNumber . realToFrac   fromJValue = doubleToJValue roundinstance JSON Integer where   toJValue = JNumber . realToFrac   fromJValue = doubleToJValue roundinstance JSON Double where   toJValue = JNumber   fromJValue = doubleToJValue id我们可以在任意地方添加新的实例;并不限制在类型类声明的那个模块中。类型类的这个特性就是它的开放世界假设。如果有方法表示“这个类型类只存在下面这些实例”的概念,将得到一个封闭的世界。我们希望可以把列表转成JSON中的数组。现在还不需要担心实现的细节,因此我们用undefined作为实例方法的函数体。-- file: ch06/BrokenClass.hsinstance (JSON a) => JSON [a] where   toJValue = undefined   fromJValue = undefined把名/值对转成JSON对象也很方便。-- file: ch06/BrokenClass.hsinstance (JSON a) => JSON [(String, a)] where   toJValue = undefined   fromJValue = undefined什么时候重叠实例会造成问题?如果把这些定义放到文件中,并在ghci里载入,开始看上去都是正常的。ghci> :load BrokenClass[1 of 2] Compiling SimpleJSON       ( ../ch05/SimpleJSON.hs, interpreted )[2 of 2] Compiling BrokenClass      ( BrokenClass.hs, interpreted )Ok, modules loaded: SimpleJSON, BrokenClass.然而,一旦使用数对的列表实例时,就会出现麻烦。ghci> toJValue [("foo","bar")]:1:0:   Overlapping instances for JSON [([Char], [Char])]     arising from a use of `toJValue' at :1:0-23   Matching instances:     instance (JSON a) => JSON [a]       -- Defined at BrokenClass.hs:(44,0)-(46,25)     instance (JSON a) => JSON [(String, a)]       -- Defined at BrokenClass.hs:(50,0)-(52,25)   In the expression: toJValue [("foo", "bar")]   In the definition of `it': it = toJValue [("foo", "bar")]这个重叠实例的问题是Haskell的开放世界假定造成的。这里有个简单的例子可以更清楚的看出问题所在。-- file: ch06/Overlap.hsclass Borked a where   bork :: a -> Stringinstance Borked Int where   bork = showinstance Borked (Int, Int) where   bork (a, b) = bork a ++ ", " ++ bork binstance (Borked a, Borked b) => Borked (a, b) where   bork (a, b) = ">>" ++ bork a ++ " " ++ bork b ++ "



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3